Vue组件、插槽、Ref
组件化规范
Web Components 通过创建封装好功能的定制元素解决上述问题
- 尽可能多的重用代码
- 自定义组件的方式不太容易(html、css和js)
- 多次使用组件可能导致冲突
一些创建规则:
- data需要使用一个函数来返回对象
- 组件模板内容必须是单个 根元素(只能有一个根元素,内部可以写其他元素)
全局组件注册
语法
Vue.component(组件名称, {
data: 组件数据,
template: 组件模板内容
})
例如:
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function(){
return {
count: 0
}
},
template: "<button @click='count++'>点击了{{count}}次</button>"
})
上面直接写在点击事件里的语句,其实可以通过调用函数来完成
// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
data: function(){
return {
count: 0
}
},
template: "<button @click='handle'>点击了{{count}}次</button>",
methods: {
handle: function(){
this.count++
}
}
})
使用
<button-counter></button-counter>
<!-- 不同的组件,相互之间的数据是独立的 -->
<button-counter></button-counter>
局部组件注册
局部组件只能在注册它的父组件里使用
let componentA = {
data: function () {
return {
count: 0
}
},
template: `
<div>
<button @click='count++'>点击了{{count}}次</button>
<button>测试</botton>
</div>
`
}
let vm = new Vue({
el: '#app',
components: {
'component-a': componentA
}
})
组件里的 data
为什么在vue的组件中,data要用function返回对象呢?
在创建或注册模板的时候,传入一个data属性作为用来绑定的数据。但是在组件中,data必须是一个函数,而不能直接把一个对象赋值给它。这是vm实例和组件的最大区别
Vue.component('my-component', {
template: '<div>OK</div>',
data() {
return {} // 返回一个唯一的对象,不要和其他组件共用一个对象进行返回
},
})
当一个组件被定义, data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。
模板字符串
因为直接在template
里通过文本写html可读性比较差,所以使用 模板字符串 (就是 ` 符号)
这个是ES6的新特性,优点就是可以换行和使用 ${ }
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: `
<div>
<button @click='count++'>点击了{{count}}次</button>
<button>测试</botton>
</div>
`
})
x-template 模板
就是以 #
开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板。常用的技巧是用 <script type="x-template">
包含模板。
好处就是不用写字符串了(字符串无法整理代码)
<script type="text/x-template" id="my-component">
<div>这是组件的内容</div>
</script>
组件的命名方式
短横线方式
Vue.component('my-component',{ /* ... */})
驼峰式方式(少用)
Vue.component('MyComponent',{ /* ... */})
注意:如果使用驼峰式命名组件,那么在使用组件的时候,只能在字符串模板中使用驼峰式命名,而不能直接写在页面上,因为 dom元素是不区分大小写的,所以会自动把命名转化成小写
例如MyComponent
会被转成mycomponent
这样Vue就无法找到这个组件了,因此尽量使用短横线的方式
组件的使用
因为不同的组件就是不同的实例,所以无法直接传值,需要通过其他的方法接收到外部的数据
- 父组件向子组件传值:通过属性来传值
- 子组件向父组件传值:通过自定义事件来传值
父组件向子组件传值
组件内部通过props接收传递过来的值
Vue.component('menu-item',{
props: ['title'],
template: '<div>{{title}}</div>'
})
父组件通过属性将值传递给子组件
<menu-item :title="title"></menu-item>
例子:
这个例子中的父组件指的是Vue的实例对象,也就是#app
其中子组件先在 props 里声明参数传进来的入口属性叫啥
然后子组件再 通过props里定义的属性 来接收父组件的值
<div id="app">
<div>{{pmsg}}</div>
<!-- 注意这里要使用 v-bind来绑定属性,否则只能传递静态写死的值 -->
<menu-item :title='pmsg'></menu-item>
</div>
// 子组件
Vue.component('menu-item', {
props: ['title'],
data: function () {
return {
msg: '子组件本身的数据'
}
},
template: `<div>{{msg + '-------' + title}}</div>`
})
// 父组件
let vm = new Vue({
el: '#app',
data: {
pmsg: '父组件中的内容'
}
})
遍历父组件数组
<div id="app">
<!-- 父组件传值给子组件通过属性 -->
<test-a :list='list'></test-a>
</div>
Vue.component('test-a', {
props:['list'],
template: `
<div>
<li v-for="item in list" :key="item.id">{{item.name}}</li>
</div>
`
})
let vm = new Vue({
el: '#app',
data: {
list: [
{ id: 1, name: 'apple' },
{ id: 2, name: 'orange' },
{ id: 3, name: 'banana' }
]
}
})
$emit 和自定义事件
官方文档--emit 官方文档--components-custom-events
/**
*
* @param {string} eventName
* @param {[...args]} param1
*/
vm.$emit( eventName, […args] )
作用:触发当前实例上的事件。附加参数都会传给监听器回调。
所谓的自定义事件,实际上就是对一些已有的操作进行封装,例如下面这个例子就算是自定义了一个叫做 welcome
的事件
原理就是在自组件中当触发click
之后通过$emit
发射一个welcome
事件已经触发的信号 给外部绑定了welcome
事件的v-on
而v-on
实际上就是绑定了vm.$on( event, callback )
v-on 官方文档
$on
:监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。
示例:
Vue.component('welcome-button', {
template: `
<button v-on:click="$emit('welcome')">
Click me to be welcomed
</button>
`
})
绑定这个事件,使事件触发时执行sayHi
函数
<div id="emit-example-simple">
<welcome-button @welcome="sayHi"></welcome-button>
</div>
new Vue({
el: '#emit-example-simple',
methods: {
sayHi: function () {
alert('Hi!')
}
}
})
子组件向父组件传值
流程如下:
子组件通过自定义事件向父组件传递信息
<button @click='$emit("enlarge-text")'>扩大字体</button>
父组件监听子组件的事件
<menu-item @enlarge-text='fontSize += 0.1'></menu-item>
例子:
这里通过v-on
监听自定义事件enlarge-text
是否触发
<div id="app">
<div :style='{fontSize: fontSize + "px"}'>HELLO WORLD</div>
<!-- 让自定义事件绑定 handle 函数 -->
<menu-item :parr='parr' @enlarge-text='handle'></menu-item>
</div>
这里就是 通过$emit
发射一个自定义事件已经触发的消息 来告诉外部绑定的 v-on
事件已经触发
如下自定义的事件enlarge-text
实际就是封装了click
Vue.component('menu-item', {
props: ['parr'],
template: `
<div>
<ul>
<li v-for="(item, index) in parr" :key="index">{{item}}</li>
</ul>
<button @click='parr.push("lemon")'>点击添加柠檬</button>
<button @click='$emit("enlarge-text")'>扩大父组件中字体大小</button>
</div>`
})
let vm = new Vue({
el: '#app',
data: {
parr: ['apple', 'orange', 'banana'],
fontSize: 10
}, methods: {
handle: function () {
// 扩大字体大小
this.fontSize += 5
}
},
})
非父子组件传值
原理就是通过一个专门的事件中心来管理组件间的通信
- 单独的事件中心管理组件间的通信
var eventHub = new Vue()
- 监听事件与销毁事件
eventHub.$on('add-todo',addTodo)
eventHub.$off('add-todo')
其中销毁事件$off
,参考自官方文档
作用:移除自定义事件监听器。 就是使 $on
不再监听该事件
用法:
- 如果没有提供参数,则移除所有的事件监听器;
- 如果只提供了事件,则移除该事件所有的监听器;
- 如果同时提供了事件与回调,则只移除这个回调的监听器。
- 触发事件
eventHub.$emit('add-todo',id)
例子
<div id="app">
<!-- 按下这个销毁事件,两个事件都无法传输了 -->
<button @click="handle">销毁事件</button>
<a-tom></a-tom>
<b-tom></b-tom>
</div>
// 创建一个事件中心来管理事件
let hub = new Vue()
// 组件A
Vue.component('a-tom', {
data: function () {
return {
num: 0
}
},
template: `
<div>
<div>A:{{num}}</div>
<div>
<button @click='handle'>点击后发射给B</button>
</div>
</div>`,
methods: {
handle: function () {
// 通过这个函数来发射事件(告诉对面的接收器,当前已经触发了这个事件)
hub.$emit('b-event', 2)
}
},
mounted: function () {
// 通过钩子函数来监听事件(因为这个钩子函数在实例化好后,对象时就会执行)
hub.$on('a-event', (val) => {
this.num += val
})
}
})
// 组件B
Vue.component('b-tom', {
data: function () {
return {
num: 0
}
},
template: `
<div>
<div>B:{{num}}</div>
<div>
<button @click='handle'>点击后发射给A</button>
</div>
</div>`,
methods: {
handle: function () {
// 通过这个函数来发射事件
hub.$emit('a-event', 1)
}
},
mounted: function () {
// 通过钩子函数来监听事件(因为这个钩子函数在实例化好后,对象时就会执行)
hub.$on('b-event', (val) => {
this.num += val
})
}
})
// 这个不能省略,因为需要一个Vue实例绑定一个节点,上面的操作都是全局的
let vm = new Vue({
el: '#app',
methods:{
handle:function(){
// 销毁事件
hub.$off('a-event')
hub.$off('b-event')
}
}
})
props 属性名规则
这个和上面组件名的命名规则一样,驼峰式也有那些坑 所以尽量使用短横线的形式
同样:字符串形式的模板没有这个限制
插槽slot
插槽基本使用
参考官网插槽
组件插槽的作用 就是父组件向子组件传递内容
插槽的位置:就是直接丢<slot>
标签到模板里面
Vue.component('a-tom', {
template: `
<div>
<slot></slot>
</div>`
})
插槽的内容就是一些写在标签之间的信息,这个信息可以是模板代码,或者html等等
例如下面的这个插入的就是 Hello slot
<h1>Hello slot</h1>
插槽里也可以填充一些默认内容,例如下面这个,当模板中间没有填入内容的话,默认就是显示 this is slot
<slot>this is slot</slot>
具名插槽
参考自文档
例如:如果直接使用多个普通插槽,那么内容还是重复的
<div id="app">
<test-a>hello slot</test-a>
</div>
Vue.component('test-a', {
template: `
<div>
<slot></slot>
<slot></slot>
</div>`
})
结果如图,就只是单纯的重复了一遍而已
所以当模板里需要使用到内容各不相同的插槽时,就可以使用具名插槽
元素有一个特殊的 attribute:name
。这个属性可以用来定义额外的插槽:
<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
<slot name="header"></slot>
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
<slot></slot>
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
<slot name="footer"></slot>
</footer>
</div>
具名插槽的用法也有一些不同,因为是通过属性来标识的,所以传值时也需要指定属性
在向具名插槽提供内容的时候,在一个 template
元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称:
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
注意:旧版的是直接使用 slot
新版的属性名变成了 v-slot
,且 v-slot
只能添加在 template
上,旧版的可以写在任意的标签上
作用域插槽
参考自官方文档 参考资料 深入理解vue中的slot与slot-scope
应用场景:父组件对子组件的内容进行加工处理(就是父组件中可以获取到子组件的数据)
插槽内容
<!-- 父组件 -->
<fruit-list :list="list">
<!-- slot-scope 可以得到插槽定义绑定的属性(:item="item") -->
<template slot-scope="slotProps">
<!-- strong 标签是用来高亮显示的 -->
<strong v-if="slotProps.info.id==2">
{{slotProps.info.name}}
</strong>
<!-- 否则不高亮 -->
<span v-else>{{slotProps.info.name}}</span>
</template>
</fruit-list>
插槽定义
Vue.component('fruit-list', {
props: ['list'],
template: `
<ul>
<li v-for="item in list" :key="item.id">
<slot :info="item">
{{item.name}}
</slot>
</li>
</ul>
`,
data: function () {
return {
list: [{
id: 1,
name: 'apple'
},{
id: 2,
name: 'orange'
},{
id: 3,
name: 'banana'
}]
}
},
})
Ref 获取 DOM 或 组件
在 Vue 中一般很少会用到直接操作 DOM,但不可避免有时候需要用到,这时我们可以通过 ref
和 $refs
这两个来实现($refs
是一个对象,持有已注册过 ref
的所有的子组件)
ref
有三种用法:
ref
加在普通的元素上,用this.$refs.name
获取到的是dom元素。ref
加在子组件上,用this.ref.name
获取到的是组件实例,可以使用组件的所有方法。- 利用
v-for
和ref
获取一组数组或者 DOM 节点。
<div id="app">
<input type="button" value="获取h3的值" @click="getElement()">
<h3 id="myh3" ref="myh3" >我是一个h3</h3>
<hr>
<login ref='mylogin'></login>
</div>
<script>
var login = {
template: "<h3>我是login子组件</h3>",
data(){
return {
msg: "ok"
}
},
methods:{
show(){ console.log("show方法执行了...") }
}
}
var vm = new Vue({
el: "#app",
data: {},
methods: {
getElement(){
// 通过 getElementById 方式获取 DOM 对象
// console.log(document.getElementById("myh3").innerHTML)
// console.log(this.$refs.myh3.innerHTML)
console.log(this.$refs.mylogin.msg)
this.$refs.mylogin.show()
}
},
components:{
login
}
})
</script>
devtools 调试工具
devtools 是官方的一个调试工具
github--devtools 官网讲的有点复杂了,实际就是下载一个Chrome插件而已,可以直接在下面的谷歌商店下载安装 参考的中文教程 安装的Chrome插件
视频教程参考 黑马教程